﻿// -----------------------------------------------------------------------
// <copyright file="Client.cs" company="DarkBone">
// DarkBone
// </copyright>
// -----------------------------------------------------------------------

namespace DarkBone.Engine.Networking
{
   using System;
   using System.Diagnostics;
   using System.Linq;
   using System.Net;
   using System.Net.Sockets;

   /// <summary>
   /// Networking Client
   /// </summary>
   public class Client
   {
      /// <summary>
      /// Client message
      /// </summary>
      private readonly Message clientMessage;

      /// <summary>
      /// Host info for Connect method
      /// </summary>
      private HostInfo hostInfo;

      /// <summary>
      /// Initializes a new instance of the <see cref="Client"/> class.
      /// </summary>
      public Client()
      {
         this.clientMessage = new Message();
      }

      /// <summary>
      /// Localize host
      /// </summary>
      public void MulticastLocalize()
      {
         using (
            var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
               {
                  ReceiveTimeout = 5000
               })
         {
            var ipep = new IPEndPoint(IPAddress.Any, Server.MulticastPort);
            try
            {
               Debug.WriteLine("Multicast listening");
               socket.Bind(ipep);
               socket.SetSocketOption(
                  SocketOptionLevel.IP,
                  SocketOptionName.AddMembership,
                  new MulticastOption(new IPAddress(Server.MulticastAddress), IPAddress.Any));
               var receiveBuffer = new byte[1024];
               var bytesReceived = socket.Receive(receiveBuffer);
               Debug.WriteLine("Multicast received bytes {0}", bytesReceived);
               var container = new byte[bytesReceived];
               Array.Copy(receiveBuffer, container, bytesReceived);
               this.hostInfo = Serializator.DeserializeObj<HostInfo>(container);
               Debug.WriteLine("Multicast hostinfo {0}", this.hostInfo.HostName);
            }
            catch (Exception ex)
            {
               Debug.WriteLine(ex.Message);
            }
            finally
            {
               socket.Close();
            }
         }
      }

      /// <summary>
      /// Stops this instance.
      /// </summary>
      public void Stop()
      {
         this.clientMessage.Socket.Close();
      }

      /// <summary>
      /// Connects to server and queries it
      /// </summary>
      /// <typeparam name="T">
      /// type of the object
      /// </typeparam>
      /// <param name="obj">
      /// The obj with query information
      /// </param>
      /// <returns>
      /// The object returned by server.
      /// </returns>
      public T Query<T>(object obj)
      {
         if ((this.clientMessage == null) || (this.clientMessage.Socket == null) || (!this.clientMessage.Socket.Connected))
         {
            do
            {
               this.MulticastLocalize();
            }
            while (this.hostInfo.Addresses == null);
            this.Connect();
         }

         var serializedObject = Serializator.SerializeObj(obj);
         try
         {
            this.clientMessage.Clean();
            this.clientMessage.Locker.Reset();
            var buffer = new byte[serializedObject.Buffer.Length + 4];
            Array.Copy(BitConverter.GetBytes(serializedObject.Buffer.Length), buffer, 4);
            Array.Copy(serializedObject.Buffer, 0, buffer, 4, serializedObject.Buffer.Length);
            this.clientMessage.Length = buffer.Length;
            this.clientMessage.Socket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, Machinery.OnSend, this.clientMessage);
            this.clientMessage.Locker.WaitOne();
            this.clientMessage.Clean();
            this.clientMessage.Locker.Reset();
            this.clientMessage.Socket.BeginReceive(this.clientMessage.Buffer, 0, this.clientMessage.Buffer.Length, SocketFlags.None, Machinery.OnReceive, this.clientMessage);
            this.clientMessage.Locker.WaitOne();
            return Serializator.DeserializeObj<T>(this.clientMessage.TransmissionBuffer.ToArray());
         }
         catch (Exception ex)
         {
            Serializator.DeserializeObj<T>(this.clientMessage.TransmissionBuffer.ToArray());
            Debug.WriteLine("Query Exception: {0}", ex);
            return (T)obj;
         }
      }
      
      /// <summary>
      /// Deserializes and outputs the received object
      /// </summary>
      /// <param name="message">The message.</param>
      private static void OnDone(Message message)
      {
         message.Locker.Set(); 
      }

      /// <summary>
      /// Starts when the connection was accepted by the remote hosts and prepares to send data
      /// </summary>
      /// <param name="result">The result.</param>
      private static void OnConnect(IAsyncResult result)
      {
         var message = (Message)result.AsyncState;
         message.Done = OnDone;
         message.Socket.EndConnect(result);
         message.Locker.Set();
      }

      /// <summary>
      /// Connects this instance.
      /// </summary>
      private void Connect()
      {
         this.clientMessage.Locker.Reset();
         this.clientMessage.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
         foreach (var address in this.hostInfo.Addresses.Where(address => address.AddressFamily == AddressFamily.InterNetwork))
         {
            this.clientMessage.Socket.BeginConnect(new IPEndPoint(address, Server.UnicastPort), OnConnect, this.clientMessage);
            this.clientMessage.Locker.WaitOne();
            if (this.clientMessage.Socket.Connected)
            {
               break;
            }
         }
      }
   }
}
